/**
 * 注意，此文件是桌面端和移动端共用的，所以不应引入 antd / antd-mobile 的内容
 * 不然在开发环境下，会导致打包进很多不必要的代码
 */
import { type MaySuccess, success, failed } from '@gnlab/utils'
import type { MemSubject, MemStream } from '@gnlab/utils/rx'
import dayjs, { type Dayjs } from 'dayjs'
import escapeRegExp from 'lodash/escapeRegExp'
import flatMap from 'lodash/flatMap'
import throttle from 'lodash/throttle'
import { BehaviorSubject, type Observable, type UnaryFunction, type Subscription } from 'rxjs'

export function unique<T>(list: T[]) {
  return [...new Set(list)]
}

/**
 * 高亮关键词
 */
export function highlightKeyword(nodes: (string | JSX.Element)[], keyword: string, color?: string) {
  const regexp = new RegExp(escapeRegExp(keyword.trim()), 'i')
  const style = color !== undefined ? { color } : {}

  return flatMap(nodes, (node, i) => {
    if (typeof node !== 'string') return [node]
    const split = node.split(regexp)
    if (split.length === 1) return split

    const highlight = (key: string) => <span className="highlight" style={style} key={key}>
      {keyword}
    </span>
    const formatted = flatMap(split, (item, j) => {
      if (j === split.length - 1) return [item]
      return [item, highlight(`high-${i}-${j}`)]
    })

    return formatted
  })
}

type Obs<T = unknown> = Observable<T>
type ObsV<T> = T extends Observable<infer V> ? V : unknown
type Op<A extends Obs, B extends Obs> = UnaryFunction<A, B>

/*
RxStream: 数据流
- 和 BehaviorSubject 一样，能实时读取数据，也能订阅数据推送；但不提供 next() 方法，因此可安全开放给外界。
- 初始化时便会订阅上级数据源，除非手动调用 unsubscribeSource() 否则不会终止；多个使用者订阅 stream 不会影响对上级数据源的订阅。

本质上就是一个禁用了 next() 方法、有固定的上级数据源的 BehaviorSubject。
非常适合用在业务逻辑层里，作为提供给外界的“数据源”。
*/
export class RxStream<T> extends BehaviorSubject<T> {
  private readonly _source: Observable<T> // 为避免和 BehaviorSubject 自己的 source 字段冲突，添加 _ 前缀
  private sourceSubscription!: Subscription | null

  constructor(source: BehaviorSubject<T>)
  constructor(source: Observable<T>, defaults: T)
  constructor(source: BehaviorSubject<T> | Observable<T>, defaults?: T) {
    const initialValue = 'value' in source ? source.value : defaults!
    super(initialValue)

    this._source = source
    this.subscribeSource()
  }

  /**
   * 如果不想 stream 再推送数据了，可调用此方法，以节约资源。
   */
  unsubscribeSource() {
    this.sourceSubscription?.unsubscribe()
    this.sourceSubscription = null
  }

  /**
   *
   * 若调用 unsubscribeSource 后想恢复推送，可调用此方法。
   * （stream 初始化推送也是使用此方法）
   */
  subscribeSource() {
    if (!this.sourceSubscription) {
      let initial = true
      this.sourceSubscription = this._source.subscribe(value => {
        // source 第一次推送值时，如果值和 stream 当前值一样，不推送给 stream 的监听者。
        // 这样可保证 unsubscribeSource() 后重新 subscribeSource() 时，不触发不必要的推送。
        if (initial) {
          initial = false
          if (value === this.value) return
        }
        this.next(value)
      })
    }
  }

  /**
   * 执行 pipe，并将结果封装成 stream。
   */
  pipe2stream<X extends T>(): RxStream<X>
  pipe2stream<A extends Obs>(op1: Op<this, A>, defaults: ObsV<A>): RxStream<ObsV<A>>
  pipe2stream<A extends Obs, B extends Obs>(op1: Op<this, A>, op2: Op<A, B>, defaults: ObsV<B>): RxStream<ObsV<B>> // prettier-ignore
  pipe2stream<A extends Obs, B extends Obs, C extends Obs>(op1: Op<this, A>, op2: Op<A, B>, op3: Op<B, C>, defaults: ObsV<C>): RxStream<ObsV<C>> // prettier-ignore
  pipe2stream<A extends Obs, B extends Obs, C extends Obs, D extends Obs>(op1: Op<this, A>, op2: Op<A, B>, op3: Op<B, C>, op4: Op<C, D>, defaults: ObsV<D>): RxStream<ObsV<D>> // prettier-ignore
  pipe2stream<A extends Obs, B extends Obs, C extends Obs, D extends Obs, E extends Obs>(
    op1: Op<this, A>, op2: Op<A, B>, op3: Op<B, C>, op4: Op<C, D>, op5: Op<D, E>, defaults: ObsV<E>): RxStream<ObsV<E>> // prettier-ignore
  pipe2stream<A extends Obs, B extends Obs, C extends Obs, D extends Obs, E extends Obs, F extends Obs>(
     op1: Op<this, A>, op2: Op<A, B>, op3: Op<B, C>, op4: Op<C, D>, op5: Op<D, E>, op6: Op<E, F>, defaults: ObsV<F>): RxStream<ObsV<F>> // prettier-ignore
  pipe2stream<A extends Obs, B extends Obs, C extends Obs, D extends Obs, E extends Obs, F extends Obs, G extends Obs>(
     op1: Op<this, A>, op2: Op<A, B>, op3: Op<B, C>, op4: Op<C, D>, op5: Op<D, E>,  op6: Op<E, F>, op7: Op<F, G>, defaults: ObsV<G>): RxStream<ObsV<G>> // prettier-ignore
  pipe2stream<A extends Obs, B extends Obs, C extends Obs, D extends Obs, E extends Obs, F extends Obs, G extends Obs, H extends Obs>(
     op1: Op<this, A>, op2: Op<A, B>, op3: Op<B, C>, op4: Op<C, D>, op5: Op<D, E>, op6: Op<E, F>, op7: Op<F, G>, op8: Op<G, H>, defaults: ObsV<H>): RxStream<ObsV<H>> // prettier-ignore
  pipe2stream<A extends Obs, B extends Obs, C extends Obs, D extends Obs, E extends Obs, F extends Obs, G extends Obs, H extends Obs, I extends Obs>(
     op1: Op<this, A>, op2: Op<A, B>, op3: Op<B, C>, op4: Op<C, D>, op5: Op<D, E>, op6: Op<E, F>, op7: Op<F, G>, op8: Op<G, H>, op9: Op<H, I>, defaults: ObsV<I>): RxStream<ObsV<I>> // prettier-ignore
  pipe2stream<X extends Obs>(...args: unknown[]) {
    if (args.length < 2) return this
    const defaults = args.pop() as T
    const ops = args as []
    return new RxStream(this.pipe(...ops), defaults) as RxStream<ObsV<X>>
  }
}

/**
 * 获取 streams 的最新值
 * streams 中的任意一个可以替换，但总数不能变（useEffect 机制不允许）
 */
type MT<T> = MemSubject<T> | MemStream<T> | BehaviorSubject<T> | RxStream<T>
function useStreams<T1>(): MT<T1>[]
function useStreams<T1>(a: MT<T1>): [T1]
function useStreams<T1, T2>(a: MT<T1>, b: MT<T2>): [T1, T2]
function useStreams<T1, T2, T3>(a: MT<T1>, b: MT<T2>, c: MT<T3>): [T1, T2, T3]
function useStreams<T1, T2, T3, T4>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>): [T1, T2, T3, T4]
function useStreams<T1, T2, T3, T4, T5>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>, e: MT<T5>): [T1, T2, T3, T4, T5] // prettier-ignore
function useStreams<T1, T2, T3, T4, T5, T6>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>, e: MT<T5>, f: MT<T6>): [T1, T2, T3, T4, T5, T6] // prettier-ignore
function useStreams<T1, T2, T3, T4, T5, T6, T7>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>, e: MT<T5>, f: MT<T6>, g: MT<T7>): [T1, T2, T3, T4, T5, T6, T7] // prettier-ignore
function useStreams<T1, T2, T3, T4, T5, T6, T7, T8>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>, e: MT<T5>, f: MT<T6>, g: MT<T7>, h: MT<T8>): [T1, T2, T3, T4, T5, T6, T7, T8] // prettier-ignore
function useStreams<T1, T2, T3, T4, T5, T6, T7, T8, T9>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>, e: MT<T5>, f: MT<T6>, g: MT<T7>, h: MT<T8>, i: MT<T9>): [T1, T2, T3, T4, T5, T6, T7, T8, T9] // prettier-ignore
function useStreams<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>, e: MT<T5>, f: MT<T6>, g: MT<T7>, h: MT<T8>, i: MT<T9>, j: MT<T10>): [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10] // prettier-ignore
function useStreams<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>, e: MT<T5>, f: MT<T6>, g: MT<T7>, h: MT<T8>, i: MT<T9>, j: MT<T10>, k: MT<T11>): [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11] // prettier-ignore
function useStreams<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(a: MT<T1>, b: MT<T2>, c: MT<T3>, d: MT<T4>, e: MT<T5>, f: MT<T6>, g: MT<T7>, h: MT<T8>, i: MT<T9>, j: MT<T10>, k: MT<T11>, l: MT<T12>): [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12] // prettier-ignore
function useStreams<T>(...streams: MT<T>[]) {
  const [values, setValues] = useState(() => streams.map(stream => stream.value))

  useEffect(() => {
    // MemSubject 和 MemSubject（BehaviorSubject）会在 subscribe 时立刻推送一次当前值，因此不能在每次 handle 时都固定重新渲染，不然初始化时会渲染两次。
    // 也不能通过 .skip(1) 来避免，不然万一在第一次返回值后、useEffect 内容触发前，值发生了变更，最新的值就不会触发到 effect 里，导致页面上状态是老的。
    // 这里的办法是每次 handle 时判断新老 values 数组是否完全一致，有变化才触发重新渲染。
    function handle() {
      const nextValues = streams.map(stream => stream.value)
      setValues(values =>
        // 值没有改变则返回原 values 数组，这样不会触发 state 更新
        values.every((value, index) => nextValues[index] === value) ? values : nextValues,
      )
    }
    const subscriptions = streams.map(stream => (stream as MemSubject<T>).subscribe(handle))
    return () => subscriptions.forEach(subscription => subscription.unsubscribe())
  }, streams) // eslint-disable-line ts-react-hooks/exhaustive-deps

  return values
}
export { useStreams }

/**
 * 复制指定文本
 */
export function copy(text: string) {
  const textarea = document.createElement('textarea')
  Object.assign(textarea.style, { position: 'absolute', left: '-1000px' })
  document.body.appendChild(textarea)
  textarea.innerHTML = text
  textarea.select()
  document.execCommand('copy')
  textarea.blur()
  document.body.removeChild(textarea)
}

/**
 * 解析 JSON，若失败，返回 undefined
 */
export function safeJSONParse<T>(json: string) {
  try {
    return JSON.parse(json) as T
  } catch (e) {
    return undefined
  }
}

/**
 * 数据 JSON 化，若失败，返回其字符串形式（String(data)）
 */
export function safeJSONStringify(data: unknown) {
  try {
    return JSON.stringify(data)
  } catch (e) {
    try {
      return String(data)
    } catch (e) {
      return 'typeof: ' + typeof data
    }
  }
}

/**
 * 目前后端对时区的处理，默认的 2023-09-14T02:45:22.000Z 格式的时间最终解析出的时区会不对，
 * 需要传 2023-09-14T02:45:22.000+08:00 格式
 */
export function toISOString(time: Dayjs | Date | number) {
  if (time instanceof Date || typeof time === 'number') time = dayjs(time)
  return time.format('YYYY-MM-DDTHH:mm:ss.SSSZ')
}

/**
 * 获取指定 URL 的图片的尺寸
 */
export async function getImageInfo(imageFile: File | Blob) {
  return new Promise<{ width: number; height: number }>(resolve => {
    const url = URL.createObjectURL(imageFile)
    const img = new Image()
    img.onload = () => {
      resolve({ width: img.width, height: img.height })
      URL.revokeObjectURL(url)
    }
    img.src = url
  })
}

/**
 * 获取指定 URL 的视频的尺寸和时长
 */
export async function getVideoInfo(videoFile: File) {
  return new Promise<{ width: number; height: number; duration: number }>(resolve => {
    const url = URL.createObjectURL(videoFile)
    const video = document.createElement('video')
    video.onloadeddata = () => {
      resolve({ width: video.videoWidth, height: video.videoHeight, duration: video.duration })
      URL.revokeObjectURL(url)
    }
    video.src = url
  })
}

/**
 * 获取指定 URL 的音频的时长
 */
export async function getAudioInfo(audioFile: File) {
  return new Promise<{ duration: number }>(resolve => {
    const url = URL.createObjectURL(audioFile)
    const audio = document.createElement('audio')
    audio.onloadeddata = () => {
      resolve({ duration: audio.duration })
      URL.revokeObjectURL(url)
    }
    audio.src = url
  })
}

/**
 * 缩放图片到指定尺寸
 */
export async function scaleImage(file: File | Blob, widthLimit: number, heightLimit: number) {
  return new Promise<MaySuccess<File | Blob>>(resolve => {
    const url = URL.createObjectURL(file)
    const image = new Image()
    image.onload = () => {
      if (image.width < widthLimit && image.height < heightLimit) return void resolve(success(file))

      const canvas = document.createElement('canvas')
      canvas.style.display = 'none'
      const ctx = canvas.getContext('2d')
      if (!ctx) return void resolve(failed('canvas 创建失败'))

      const ratio = 1 / Math.max(image.width / widthLimit, image.height / heightLimit)
      const [targetWidth, targetHeight] = [image.width * ratio, image.height * ratio]
      canvas.width = targetWidth
      canvas.height = targetHeight
      ctx.drawImage(image, 0, 0, targetWidth, targetHeight)
      const dataURL = canvas.toDataURL('image/jpeg', 0.85)
      const blob = dataURItoBlob(dataURL)
      resolve(success(blob))

      URL.revokeObjectURL(url)
    }
    image.src = url
  })
}
// 来自：https://stackoverflow.com/a/11954337
function dataURItoBlob(dataURI: string) {
  const binary = atob(dataURI.split(',')[1]!)
  const array = []
  for (let i = 0; i < binary.length; i++) array.push(binary.charCodeAt(i))
  return new Blob([new Uint8Array(array)], { type: 'image/jpeg' })
}

/**
 * 把文件尺寸格式化成适合阅读的形式
 */
export function formatFileSize(size: number) {
  const round = (value: number) => Math.round(value * 10) / 10
  if (size < 1024 * 1024) return `${round(size / 1024)}K`
  if (size < 1024 * 1024 * 1024) return `${round(size / 1024 / 1024)}M`
  return `${round(size / 1024 / 1024 / 1024)}G`
}

/**
 * 调用更新函数后，经过延迟才更新 value，额外提供会实时更新的 inputValue
 *
 * 此功能常用于搜索文本框：
 * 1. 文本框内容要实时更新
 * 2. 但触发搜索却不希望实时触发
 * 此时，文本框内容使用 inputValue，搜索基于 value 即可。
 */
export function useThrottledValue<T>(initialValue: T, wait: number = 200) {
  const [inputValue, setInputValue] = useState(initialValue)

  const [value, rawSetValue] = useState(initialValue)
  const setValue = useMemo(() => throttle(rawSetValue, wait), [wait])

  const outSetValue = useCallback(
    (value: T) => {
      setInputValue(value)
      setValue(value)
    },
    [setValue],
  )

  return { value, inputValue, setValue: outSetValue }
}
